#
# CVSCore.py
#
# Core CVS Functionality Wrappers for SNiFF+ 3.x CVS Adapter
#
# Author: Martin Oberhuber
# Copyright (c) 1998, 1999 TakeFive Software, Salzburg
#
# Revisions:
# 

import os, string, regsub, regex, sys, time
import vcs
from vcs import TRUE, FALSE, NL, SYSTEM, WINDOWS, UNIX
from vcs import NULLDEV, DELIMITER
from etgui import RunCmd
from time import asctime, localtime

import CVSPrefs

# ----- helper functions -----------------------------------------------------

def relative_dir(workingDir, projectDir, catDir):
	if workingDir==catDir:
		# absolute fileType dir
		return catDir
	else:
		return projectDir

def split_path(cvs_path):
	"""split a cvs path ':method:path' into method part and path part."""
	if not cvs_path:
		return 'local', ''
	if cvs_path[0] == ':':
		# explicitly stated access method
		second_colon = string.find(cvs_path, ':', 1)
		if second_colon <= 0:
			# Path starting with a colon, but then no more colons - should be illegal
			return 'local', cvs_path[1:]
		method = cvs_path[1:second_colon]
		if method == 'local':
			path = cvs_path[second_colon+1:]
			return method, path
		# ext or server access method
		third_colon = string.find(cvs_path, ':', second_colon+1)
		if third_colon <= 0:
			path = cvs_path[second_colon+1:]
		else:
			path = cvs_path[third_colon+1:]
			method = cvs_path[1:third_colon]
		return method, path
	else:
		# no explicitly stated access method
		first_colon = string.find(cvs_path, ':')
		if first_colon < 0:
			return 'local', cvs_path
		else:
			method = cvs_path[:first_colon]
			path = cvs_path[first_colon+1:]
			return method, path

def real_dir_of_file(PWE, SSWE, catDir, wfile):
	"""find file in private or shared source workspaces."""
	if CVSPrefs.TEST2: print "(CVSCore) @r>>>@x Breal_dir_of_file (",
	if CVSPrefs.TEST2: print "PWE: ",PWE,", SSWE: ",SSWE,", wfile: ",wfile,")"
	if not SSWE:
		(dir, file) = os.path.split(wfile)
		if dir: return vcs.joinpath(PWE,dir)
		else: return PWE
	realdir = ""
	SSWEList = []
	for dir in string.split(SSWE, DELIMITER):
		if catDir and catDir != ".":
			SSWEList.append(vcs.joinpath(dir, catDir))
		else:
			SSWEList.append(dir)
	for dir in [PWE] + SSWEList:
		path = vcs.joinpath(dir, wfile)
		#if CVSPrefs.TEST2: print "(CVSCore) real_dir_of_file: testing path
		st = vcs.get_stat(path)
		if SYSTEM == WINDOWS:
			# on Windows, Links are differently recognized.
			if vcs.test_exists(st):
				(dir, file) = os.path.split(path)
				if not realdir: realdir = dir
				if GetEntry(dir, file):
					return dir
		else:
			# on UNIX, we check for symlinks
			if vcs.test_exists(st) and not os.path.islink(path):
				(dir, file) = os.path.split(path)
				return dir
	if SYSTEM == WINDOWS and realdir:
		# the file exists, but there is no CVS/Entries information in any
		# one of the shared sources!
		# return the PWE's location
		return realdir
	return 'file-does-not-exist'

def real_path_of_file(wfile):
	"""Follow symbolic links to output the real path of a file.
	BUGBUG: it might be better to stop following links as soon as
	CVS information is found in a directory. But that would probably be
	MUCH slower because CVS/Entries need to be read for each file...
	anyway, caching might help..."""
	if CVSPrefs.TEST2: print "(CVSCore) @r>>>@x Breal_path_of_file (",
	if CVSPrefs.TEST2: print "wfile: ",wfile,")"
	while 1:
		st = vcs.get_stat(wfile)
		if not vcs.test_exists(wfile):
			if CVSPrefs.TEST2: print "--> File does not exist!"
			return ""
		elif not os.path.islink(wfile):
			return wfile
		else:
			wfile = os.readlink(wfile)
			if CVSPrefs.TEST2: print "--> followed to ",wfile

def GetStatusOutput(cmd):
	"""Return (status, output, err) of executing cmd in a shell."""
	marker = 'CVSCore.GetStatusOutput:'
	if vcs.SYSTEM==vcs.WINDOWS:
		cmd = '%s -c \'%s\'' % (vcs.SHELL, cmd)
	if CVSPrefs.TEST: print marker, 'cmd: ',cmd
	try:
		rc, _out, _err = RunCmd(cmd)
	except RuntimeError, arg:
		rc, _out, _err = -1, '', 'Error during fork: '+arg
	if CVSPrefs.TEST: print marker,'result <%d,%s,%s>' % (rc,_out[:132],_err[:132])
	return rc, _out, _err

# ----- class CVSIter --------------------------------------------------------
# usage:
#	wDir = 'xyz'; wFile = 'abc'; label = 'LABEL'
#	for line in CVSIter('-d %s files %s@%s' % (wDir,wFile,label)):
#	   print line
class CVSIter:
	def __init__(self,cmd):
		# with getstatusoutput() the 2>&1 is not necessary...
		# HERE, one can add global options according to CVSPrefs
		xcmd = 'cvs ' + cmd
		try:
			#print '__init__: %f' % time.time()
			rc, self.lines, self.err = GetStatusOutput(xcmd)
			self.lines.append(self.err)
			if rc:
				if TEST:
					print 'P4Iter("%s") error code %d' % (cmd,rc)
				raise P4Error
			# read all the lines for later processing...
			self.lines = string.split(self.lines,'\n')
		except IOError:
			import traceback
			print 'CVSIter.__init__() error:'
			traceback.print_exc()
			pass

	def __getitem__(self,i):
		#print '__getitem__ for index %d: %s' % (i,self.lines[i])
		return self.lines[i]

def ParseUpdateOutput(out):
	if not out: 
		# file was removed or nothing was done
		return ""
	if out[0] == "U":
		return('-> Checked @B%s@B out' % out[2:])
	elif out[0] == "P":
		return('-> Patched @B%s@B without conflict' % out[2:])
	elif out[0] == "A":
		return('-> Uncommitted Added File @B%s@B not changed' % out[2:])
	elif out[0] == "R":
		return('-> Uncommitted Removed File @B%s@B not changed' % out[2:])
	elif out[0] == "M":
		return('-> @gMerged@x @B%s@B without conflict' % out[2:])
	elif out[0] == "C":
		return('-> @rConflict@x while merging @B%s@B' % out[2:])
	elif out[0] != "I" and out[0] != "?":
		return ('-> ?? Unknown cvs update output: '+ out)
	return ""

# ----- Class CVSEntry  -------------------------------------------------

def MakeDirEntry(dir, name):
	return CVSEntry(dir, ['D',name,'','','',''])

class CVSEntry:
	def __init__(self,d,e):
		self._entry=e
		self._dir=d
	def getDir(self):
		return self._dir
	def getName(self):
		return self._entry[1]
	def getVersion(self):
		return self._entry[2]
	def getMtime(self):
		return self._entry[3]
	def getStickyOpts(self):
		return self._entry[4]
	def getStickyTag(self):
		return self._entry[5][1:]
	def getEntryString(self):
		return string.join(self._entry,'/')
	def setStickyTag(self,tag):
		self._entry[5]="T"+tag
	def isNew(self):
		return self.getVersion()=="0"
	def isRemoved(self):
		return self.getVersion()[0]=="-"
	def isModified(self):
		return self.modified() != "="
	def modified(self):
		"""returns N for new files, R for removed files, L for lost files,
		= for unmodified and M for modified ones"""
		if self.isNew(): return "N"
		if self.isRemoved(): return "R"
		st = vcs.get_stat(vcs.joinpath(self.getDir(), self.getName()))
		if not vcs.test_exists(st): return "L"
		mt = time.asctime(time.gmtime(vcs.get_mtime(st)))
		if CVSPrefs.TEST:print "(CVSCore) CVSEntry.modified: file is ",
		if CVSPrefs.TEST:print mt, ", Entry is ", self.getMtime(), "."
		if mt==self.getMtime(): 
			return "="
		else:
			return "M"
	def write(self):
		# BUGBUG would probably be nicer to just write Entries.Log: locking?
		entries = get_entries(self._dir)
		if not entries:
			entries = { }
		# BUGBUG each entry contains its directory, this is inefficient
		entries[self.getName()] = self
		write_entries(self._dir, entries)

# ----- Class CVSEntries ------------------------------------------------

#
# GetEntry:
#	Retrieve entry of file out of CVS/Entries
#
def GetEntry(dir, file):
	if CVSPrefs.TEST: print "(CVSCore) GetEntry (", file, ", ", dir, ")"
	try:
		return get_entries(dir)[file]
	except KeyError:
		return None

#
# get_entries:
#	Load CVS/Entries file and split lines and fields
#	Result: Dictionary of CVSEntry objects indexed by name
#
_ENTRIES = {}	# entries cache
def get_entries(dir, use_cache=TRUE):
	if CVSPrefs.TEST2: print "(CVSCore) get_entries (", dir, ")" 
	entriesfile = vcs.joinpath(dir, 'CVS', 'Entries')
	st1 = vcs.get_stat(entriesfile)
	entrieslog = vcs.joinpath(dir, 'CVS', 'Entries.Log')
	st2 = vcs.get_stat(entrieslog)
	fileTime = 0
	if vcs.test_exists(st1):
		fileTime = vcs.get_mtime(st1)
	if vcs.test_exists(st2):
		fileTime = max(fileTime, vcs.get_mtime(st2))
	if not fileTime:
		if CVSPrefs.TEST2: print"(CVSCore) get_entries: No CVS/Entries file found"
		return {}
	if CVSPrefs.TEST2: print "File's mtime is ", fileTime, ": ", asctime(localtime(fileTime))
	if use_cache and _ENTRIES.has_key(dir):
		if CVSPrefs.TEST: print"(CVSCore) get_entries --> found in cache",
		(cachedTime, cachedEntries) = _ENTRIES[dir]
		if CVSPrefs.TEST2: print "Cached mtime is ", cachedTime, ": ", asctime(localtime(cachedTime)),
		if cachedTime == fileTime:
			if CVSPrefs.TEST: print ", taken"
			return cachedEntries
		else:
			if CVSPrefs.TEST: print ", but out of date"
	_entries = {}
	if vcs.test_exists(st1):
		if CVSPrefs.TEST: print "(CVSCore) get_entries: Reloading Entries of ",dir
		entries = open(entriesfile).read()
		entries = string.split(string.strip(entries), NL)
		entries = map(lambda x:string.split(x,'/'), entries)
		for entry in entries:
			if len(entry)==6:
				# don't take the single "D" and other illegal combinations into account
				_entries[entry[1]] = CVSEntry(dir,entry)
			elif len(entry)!=1:
				print "(CVSCore) get_entries: ignored illegal Entry: ",entry
	if vcs.test_exists(st2):
		if CVSPrefs.TEST: print "(CVSCore) get_entries: Reloading Entries.Log of ",dir
		el = open(entrieslog).read()
		el = string.split(string.strip(el),NL)
		for entry in el:
			xe = string.split(entry[2:],'/')
			if entry[0]=="A":
				_entries[xe[1]] = CVSEntry(dir,xe)
			elif entry[0]=="D":
				del _entries[xe[1]]
			else:
				print "Error -- Illegal Entries.Log format"
		if CVSPrefs.TEST: print "(CVSCore) get_entries: Rewriting Entries and deleting Entries.Log"
		write_entries(dir, _entries)
		st = vcs.get_stat(entriesfile)
		if vcs.test_exists(st):
			vcs.rm_f(entrieslog)
		fileTime = vcs.get_mtime(st)
	if CVSPrefs.TEST2: print "(CVSCore) get_entries: Result = ", _entries.keys()
	_ENTRIES[dir] = (fileTime, _entries)
	return _entries
 
#
# RemoveEntry:
#	remove one entry from the CVS/Entries file
#
def RemoveEntry(file):
	dir, file = os.path.split(file)
	entries = get_entries(dir)
	if entries:
		try:
			del entries[file]
			write_entries(dir, entries)
		except KeyError:
			pass

#
# write_entries:
#	rewrite entries list to disk
#
def write_entries(dir, entries):
	try:
		entriesfile = vcs.joinpath(dir, 'CVS', 'Entries')
		file = open(entriesfile, 'w')
		_entries = map(lambda x: x.getEntryString(), entries.values())
		file.write(string.join(_entries,NL) + NL)
		file.close()
	except:
		# if Entries.Log is in the SSWE, Entries will not be
		# writable -- this is not really an error!
		pass
 
#
# copy_file:
#	copy file from <src> to <dst>
#	(for small files only)
#
def copy_file(src,dst):
	c = open(src).read()
	f = open(dst,'w')
	f.write(c)
	return f.close()

#
# get_Repository:
#	returns a tuple (cvsRoot, DELIMITER, relativeRepository)
#
_REPOSITORIES = {}	# repositories cache
_LastValid = ("","")
def get_repository(workingDir):
	#if CVSPrefs.TEST2: print "(CVSCore) @r>>>@x get_repository"
	rootfile = vcs.joinpath(workingDir, "CVS", "Root")
	rootstat = vcs.get_stat(rootfile)
	if not vcs.test_exists(rootstat):
		return ""

	repfile = vcs.joinpath(workingDir, "CVS", "Repository")
	repstat = vcs.get_stat(repfile)
	if not vcs.test_exists(repstat):
		return ""

	try:
		(mtime, root, relative) = _REPOSITORIES[workingDir]
		_LastValid = (root, relative)
	except KeyError:
		mtime, root, relative = ( 0, "", "" )
	newmtime = vcs.get_mtime(rootstat)
	if vcs.get_mtime(repstat) > newmtime:
		newmtime = vcs.get_mtime(repstat)
	if newmtime != mtime:
		if CVSPrefs.TEST: print "(CVSCore) get_repository: Re-loading Repository Information for ", rootfile
		f = open(rootfile,"r")
		newroot = string.strip(f.readline())
		if CVSPrefs.ChangeRemoteRepUser:
			at_pos = string.rfind(newroot,"@")
			if at_pos>0:
				# replace the SSWE's Username with the local username!
				colon_pos = string.rfind(newroot[:at_pos],":")
				if colon_pos < 0:
					real_root = vcs.user + newroot[at_pos:]
				else:
					real_root = newroot[:colon_pos+1] + vcs.user + newroot[at_pos:]
				if CVSPrefs.TEST: print "(CVSCore) get_repository: Replace ",newroot," with ",real_root
				newroot=real_root
		method,rootpath = split_path(newroot)
		f.close()
		f = open(repfile,"r")
		newpath = string.strip(f.readline())
		f.close()
		# -LJL- relative repositories: newpath can be shorter than rootpath
		# but this is no python exception - out of bound index will cut at
		# string end.
		if newpath[:len(rootpath)] == rootpath:
			newrelative=newpath[len(rootpath)+1:]
			_LastValid = (newroot, newrelative)
			_REPOSITORIES[workingDir] = (newmtime, newroot, newrelative)
			(root, relative) = (newroot,newrelative)
		else:
			# -LJL- test if CVS/Repository is already a relative path
			if CVSPrefs.RelativeCVSReps and newpath[0] != '/' and newpath[1] != ':':
				newrelative=newpath
				_LastValid = (newroot, newrelative)
				_REPOSITORIES[workingDir] = (newmtime, newroot, newrelative)
				(root, relative) = (newroot,newrelative)
			else:
				print "(CVSCore) get_repository: Error - illegal combination of CVS/Root and CVS/Repository"
				print "Root: ",newroot," , Repository: ",newpath
				(root, relative) = ("", "")
	if CVSPrefs.TEST2: print "-->(cached) Repository is ", root, vcs.DELIMITER, relative
	return root + vcs.DELIMITER + relative

def GetLastValidRoot(RWE):
	"""Get the last CVSROOT that was correct"""
	(lastRoot, dummy) = _LastValid
	if not lastRoot:
		lastRoot=RWE
	return lastRoot

#
# write_repository:
# 	writes an internal repository definition (Root DELIMITER Localpath)
#	to the CVS dir and updates the Repository Cache. 
#	Any existing repository definition is overwritten.
#
def write_repository(workingDir, cvsroot, relative):
	if CVSPrefs.TEST: print "(CVSCore) @r>>>@x write_repository in ", workingDir
	cvsdir = vcs.joinpath(workingDir, "CVS")
	vcs.mkdir_p(cvsdir)
	if vcs.is_writable(cvsdir):
		method, root= split_path(cvsroot)
		f = open(vcs.joinpath(workingDir, "CVS", "Root"),'w')
		f.write(cvsroot + NL)
		f.close()	#BUGBUG Errorhandling?
		f = open(vcs.joinpath(workingDir, "CVS", "Repository"),'w')
		f.write(vcs.joinpath(root, relative)+NL)
		f.close()
		# Repository information is not cached if the directory was not writable
		_REPOSITORIES[workingDir] = ( time.time(), cvsroot, relative )

#
# find_repository:
#	returns our internal repository definition or "" (empty String).
#	Tries to find Repository / Root definition in the Shared Source.
#	If it is found there, it's written to the local working directory (if writable).
#
def find_repository(workingDir, SSWS, catDir):
	#if CVSPrefs.TEST2: print "(CVSCore) @r>>>@x find_repository"
	cvsrep = get_repository(workingDir)
	if not cvsrep and (workingDir != catDir) and SSWS:
		# only look in SSWE if the catDir is no absolute path
		for curDir in string.split(SSWS, DELIMITER):
			cvsrep = get_repository(vcs.joinpath(curDir, catDir))
			if cvsrep: break
		if cvsrep:
			(root, relative) = string.split(cvsrep, vcs.DELIMITER)
			write_repository(workingDir, root, relative)
	return cvsrep

def GuessRepositoryFromRCCS(RWE, RCCSPath):
	if RCCSPath[:len(RWE)] == RWE:
		RCCSPath=RCCSPath[len(RWE):]
	if RCCSPath:
		res=string.split(RCCSPath, vcs.DELIMITER)
		if len(res)==2:
			(root, relative) = (res[1], res[2])
		else:
			# relative path without CVSROOT given
			(root, relative) = (GetLastValidRoot(RWE), res[0])
			return 
	else:
		return None

def GuessRepository(RWE, RCCSPath):
	"""Try to guess a CVSROOT / relative path combination:
		(a) by looking at the RCCSPATH
		(b) by looking up one directory, if we can find a Repository file there
		(c) by looking at the main projectDir's settings and applying the catDir
		(d) by direcly trying the projectDir / fileType Dir.
		Offer the user a Dialog to correct the guess."""
	return GuessRepositoryFromRCCS()

#
# InitProjects:
#	Initialize projects
#
def InitProjects(absProjDir, RWE, PWE, SSWEs):
	if not SSWEs:
		return
	SSWE = string.split(SSWEs, DELIMITER)[0]
	privateDir = vcs.joinpath(PWE, projDir)
	sharedDir = vcs.joinpath(SSWE, projDir)
	os.chdir(privateDir)
	if os.path.exists('CVS'):
		print "(skipped)"
		# XXX merge new entries of SSWE
		return
	if os.path.exists(vcs.joinpath(sharedDir, 'CVS')):
		os.mkdir('CVS')
		copy_file(vcs.joinpath(sharedDir,'CVS','Root'),
					vcs.joinpath(privateDir,'CVS','Root'))
		copy_file(vcs.joinpath(sharedDir,'CVS','Repository'),
					vcs.joinpath(privateDir,'CVS','Repository'))
		sharedEntries = get_entries(sharedDir)
		privateEntries = []
		for entry in entries:
			if os.path.exists(entry[1]) and not os.path.islink(entry[1]):
				privateEntries.append(entry)
		write_entries(privateDir, privateEntries)
		# XXX process Entries.Log ?
		return
	else:
		print 'No Shared CVS information available yet!'
		print 'Please check out from CVS yourself, e.g. using:'
		print 'cd %s/.. ; cvs co %s' % (privateDir, os.basename(privateDir))
		return
 
	# XXX process recursively

#
# print_vars:
#	print all vcs adapter variables
#
def print_vars():
	#print "%A repositoryFile : ", vcs.repositoryFile
	#print "%B branchName     : ", vcs.branchName
	#print "%b branchFlag     : ", vcs.branchFlag
	#print "%C comment        : ", vcs.comment
	#print "%D date           : ", vcs.date
	print "%F workingFile    : ", vcs.workingFile
	print "%f catDir         : ", vcs.catDir
	print "%H SSWE           : ", vcs.SSWE
	print "%G SSWS           : ", vcs.SSWS
	#print "%g SOWS           : ", vcs.SOWS
	#print "%I description    : ", vcs.description
	#print "%i pid            : ", vcs.pid
	#print "%L lockType       : ", vcs.lockType
	print "%N projectName    : ", vcs.projectName
	print "%P repositoryDir  : ", vcs.repositoryDir
	print "%p RCCSPath       : ", vcs.RCCSPath
	print "%R repositoryRoot : ", vcs.repositoryRoot
	print "%r projectDir     : ", vcs.projectDir
	#print "%S symbolName     : ", vcs.symbolName
	#print "%s forceFlag      : ", vcs.forceFlag
	#print "%T tmpFile        : ", vcs.tmpFile
	#print "%t tmpDir         : ", vcs.tmpDir
	#print "%U user           : ", vcs.user
	#print "%V revision       : ", vcs.revision
	#print "%v revision2      : ", vcs.revision2
	print "%W workingDir     : ", vcs.workingDir
	print "%w privateRoot    : ", vcs.privateRoot
	print "%d sniffDir       : ", vcs.sniffDir
	#print "%e OSTempDir      : ", vcs.OSTempDir


if __name__ == "__main__":
	retval = 1
	print retval
	x = real_dir_of_file("/softs/sniff", "/projects", ".", "test.c")
	print x
